Interactive Data Visualization for Enhanced Clinical Trial Reporting


Agustin Calatroni
AXC

Monday, October 16, 2023

Abstract

Wonderful Wednesday (WW) is an initiative of the Visualization Interest Group (VIS SIG) within the Statisticians in the Pharmaceutical Industry group (PSI), focusing on enhancing data visualization skills for clinical trials. I have actively contributed to this open-source initiative, improving interactive trial reports and statistical data visualization.
During the presentation, attendees will be introduced to a comprehensive interactive subject profile comprising numerous Analysis Data Model (ADaM) datasets, offering a well-rounded view of each participant. Additionally, we will showcase dataxray, a tool designed to generate concise statistical descriptions of these datasets. We will conclude by presenting a detailed statistical analysis report displaying individual patient data and overall treatment effects on a unified dashboard.
A key benefit of these interactive reports is their ability to be easily shared (emailed, deployed on an internal company webpage, or added to GitHub pages) due to their server-free architecture. Efforts are also underway to integrate these reports into production by incorporating them into the safety monitoring system for clinical trials.

Genesis of Wonderful Wednesday
A Visualization Initiative

Interactive Reports in Focus:
Key Approaches and Features

trelliscopejs ADaM ADLB Laboratory Test Result Data

# PACKAGES
pacman::p_load(rio, tidyverse)
pacman::p_load(labelled)
pacman::p_load(trelliscopejs, plotly)

# IMPORT
adlb_orig <- import('2022-06-08/dat/adlbh.xpt')

# SUBSET
adlb <- adlb_orig %>% 
   select(USUBJID, TRTP, AGE, RACE, SEX,
          PARAM, PARAMCD, PARCAT1,
          AVISIT, AVISITN, ADY, AVAL) %>% 
   mutate(AVISIT = str_trim(AVISIT)) %>% 
   filter(PARAMCD %in% c('BASO','EOS','HCT','HGB','LYM','MCH','MCHC','MCV','MONO','PLAT','RBC','WBC')) %>% 
   filter(USUBJID %in% sample(.$USUBJID, 20) )

# RANGE
adlb_r <- adlb %>% 
   drop_na(ADY, AVAL) %>% 
   group_by(PARAMCD) %>% 
   summarise(across(c(ADY, AVAL), list(min = min, max = max)))

# NEST
adlb_nest <- adlb %>% 
   nest_by(USUBJID, TRTP, AGE, RACE, SEX, PARAM, PARAMCD, PARCAT1) %>% 
   filter(nrow(data) > 10) %>% 
   filter( !str_detect(PARAMCD, '_') ) %>% 
   right_join(adlb_r)

# GGPLOT
adlb_gg <- adlb_nest %>% 
   mutate(
      panel = list(
         ggplot(data = data,
                aes(y = AVAL, x = ADY, label = AVISIT, label2 = AVISITN ) ) +
            geom_point() +
            geom_line() +
            scale_x_continuous(name = 'Analysis Relative Day',
                               breaks = data$ADY %>% unique(),
                               labels = data$ADY %>% unique(),
                               limits = c(ADY_min, ADY_max)) +
            scale_y_continuous(name = PARAM,
                               limits = c(AVAL_min, AVAL_max)) +
            theme_bw() +
            theme( panel.grid.minor = element_blank() ) ),
      panely = list(panel %>% ggplotly() %>% config(displayModeBar = F))) 

# COG + TRELLISCOPE
adlb_gg %>%
   mutate(min = min(data$AVAL) %>% round(2),
          max = max(data$AVAL) %>% round(2),
          sd   = sd(data$AVAL) %>% round(2)) %>% 
   select(-data, -panel, -ends_with(c('_min','_max'))) %>% 
   ungroup() %>% 
   trelliscope(
      name = "ADLB trelliscope example",
      panel_col = 'panely',
      path = '_examples/trelliscopejs/trelliscope_adlb',
      state = list(sort = list(sort_spec('USUBJID', dir = 'desc'))),
      nrow = 1,
      ncol = 3,
      height = 300,
      width = 600,
      self_contained = FALSE,
      thumb = FALSE,
      auto_cog = FALSE)
ggplot(data = adlb,
       aes(y = AVAL, x = ADY, label = AVISIT, label2 = AVISITN ) ) +
   geom_point() +
   geom_line() +
   facet_trelliscope(~ USUBJID + PARAM , 
                     nrow = 2, ncol = 2,
                     scales = 'free',
                     width = 300,
                     as_plotly = FALSE) +
   theme_bw() +
   theme( panel.grid.minor = element_blank() )

reactable ADaM ADLB Laboratory Test Result Dataset

# PACKAGES
pacman::p_load(rio, tidyverse)
pacman::p_load(labelled)
pacman::p_load(reactable, reactablefmtr, plotly)

# IMPORT
adlb_orig <- import('2022-06-08/dat/adlbh.xpt')

# SUBSET
adlb <- adlb_orig %>% 
   select(USUBJID, TRTP, AGE, RACE, SEX,
          PARAM, PARAMCD, PARCAT1,
          AVISIT, AVISITN, ADY, AVAL) %>% 
   mutate(AVISIT = str_trim(AVISIT)) %>% 
   filter(PARAMCD %in% c('BASO','EOS','HCT','HGB')) %>% #'LYM','MCH','MCHC','MCV','MONO','PLAT','RBC','WBC' )) %>% 
   filter(USUBJID %in% sample(.$USUBJID, 20) )

# RANGE
adlb_r <- adlb %>% 
   drop_na(ADY, AVAL) %>% 
   group_by(PARAMCD) %>% 
   summarise(across(c(ADY, AVAL), list(min = min, max = max)))

# NEST
adlb_nest <- adlb %>% 
   nest_by(USUBJID, TRTP, AGE, RACE, SEX, PARAM, PARAMCD, PARCAT1) %>% 
   filter(nrow(data) > 10) %>% 
   filter( !str_detect(PARAMCD, '_') ) %>% 
   right_join(adlb_r)

# GGPLOT
adlb_gg <- adlb_nest %>% 
   mutate(
      panel = list(
         ggplot(data = data,
                aes(y = AVAL, x = ADY, label = AVISIT, label2 = AVISITN ) ) +
            geom_point() +
            geom_line() +
            scale_x_continuous(name = 'Analysis Relative Day',
                               breaks = data$ADY %>% unique(),
                               labels = data$ADY %>% unique(),
                               limits = c(ADY_min, ADY_max)) +
            scale_y_continuous(name = PARAM,
                               limits = c(AVAL_min, AVAL_max)) +
            theme_bw() +
            theme( panel.grid.minor = element_blank() ) ),
      panely = list(panel %>% ggplotly(width = 500, height = 250) %>% config(displayModeBar = F))) 

adlb_gg %>% 
   ungroup() %>% 
   select(USUBJID, TRTP, AGE, RACE, SEX, PARCAT1, PARAM, panely) %>% 
   reactable(.,
             bordered = TRUE,
             highlight = TRUE,
             searchable = TRUE,
             filterable = TRUE,
             pagination = FALSE,
             height = 650,
             theme = fivethirtyeight(),
             defaultColDef = colDef( vAlign = 'center',
                                     width = 125),
             columns = list(
                TRTP = colDef(
                   width = 200
                ),
                PARAM = colDef(
                   width = 325
                ),
                panely = colDef( 
                   name = 'FIG',
                   width = 75,
                   filterable = FALSE,
                   cell = function(value){
                      if (length(value)>1) htmltools::tags$button("")
                   },
                   details = function(index) {
                      .$panely[[index]]
                   }
                )
             )
   ) %>% 
   reactablefmtr::google_font(font_family = "Ubuntu Mono") %>% 
   save_reactable_test("_examples/reactable/reactable_adlb.html")

Interactive Reports Showcases:
Personal Highlights of Wonderful Wednesday’s

dataxray ADaM ADSL Subject-level Analysis Dataset

pacman::p_load(rio, tidyverse)
# devtools::install_github("agstn/dataxray")
library(dataxray)

# IMPORT
adsl_orig <- import('2022-06-08/dat/adsl.xpt')

# RStudio IDE VIEWER
adsl_orig %>% 
   make_xray() %>% 
   view_xray() %>% 
   htmltools::save_html(file = "_examples/dataxray/ADSL/Study CDISCPilot01_ADSL_viewer.html")
   
# REPORT
report_xray(data = adsl_orig,
            data_name = 'ADSL',
            study = 'Study CDISCPilot01',
            loc = '_examples/dataxray/ADSL')

dataxray ADaM ADLB Laboratory Test Result Dataset

pacman::p_load(rio, tidyverse) 
# devtools::install_github("agstn/dataxray")
library(dataxray)

# IMPORT
adlb_orig <- import('2022-06-08/dat/adlbh.xpt')

# SUBSET
adlb <- adlb_orig %>% 
   filter( !str_detect(PARAMCD, '_') ) %>% 
   select(USUBJID,  
          PARAM, AVISIT, AVISITN, ADY, AVAL) %>% 
   mutate(AVISIT = str_trim(AVISIT)) 

# RStudio IDE VIEWER
adlb %>% 
   make_xray(by = c('PARAM')) %>% 
   view_xray(by = c('PARAM')) %>% 
   htmltools::save_html(file = "_examples/dataxray/ADLB/Study CDISCPilot01_ADLB_viewer.html")

# REPORT
report_xray(data = adlb,
            data_name = 'ADLB',
            by = c('PARAM'),
            study = 'Study CDISCPilot01',
            loc = '_examples/dataxray/ADLB')

From Visualization to Voice:
Wonderful Wednesday’s Panel Video Discussions

From Static to Interactive:
A Walkthrough of My Contributions to Wonderful Wednesday

Thanks!

Questions  

Slides & Code   github.com/agstn/RPharma23